package com.example.androidmaze;

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;

import android.content.Intent;
import android.util.Log;
import android.view.View;

/**
 * Class handles the user interaction for the maze. 
 * It implements a state-dependent behavior that controls the display and reacts to key board input from a user. 
 * After refactoring the original code from an applet into a panel, it is wrapped by a MazeApplication to be a java application 
 * and a MazeApp to be an applet for a web browser. At this point user keyboard input is first dealt with a key listener
 * and then handed over to a Maze object by way of the keyDown method.
 *
 * This code is refactored code from Maze.java by Paul Falstad, www.falstad.com, Copyright (C) 1998, all rights reserved
 * Paul Falstad granted permission to modify and use code for teaching purposes.
 * Refactored by Peter Kemper
 */
// MEMO: original code: public class Maze extends Applet {
//public class Maze extends Panel {
public class Maze  {
	private static final String TAG = "Maze";
	public int[][] savedMazeDists;
	private View view;
	// Model View Controller pattern, the model needs to know the viewers
	// however, all viewers share the same graphics to draw on, such that the share graphics
	// are administered by the Maze object
	final private ArrayList<Viewer> views = new ArrayList<Viewer>() ; 
	MazePanel panel ; // graphics to draw on, shared by all views

		
	private int distTravelled = 0;
//State changed from private int to class
	//private int state;			// keeps track of the current GUI state, one of STATE_TITLE,...,STATE_FINISH, mainly used in redraw()
	
	//dontneed StateManager state = new StateManager();
	
	// possible values are defined in Constants
	// user can navigate 
	// title -> generating -(escape) -> title
	// title -> generation -> play -(escape)-> title
	// title -> generation -> play -> finish -> title
	// STATE_PLAY is the main state where the user can navigate through the maze in a first person view

	protected int percentdone = 0; // describes progress during generation phase
	protected boolean showMaze;		 	// toggle switch to show overall maze on screen
	protected boolean showSolution;		// toggle switch to show solution in overall maze on screen
	protected boolean solving;			// toggle switch 
	protected boolean mapMode; // true: display map of maze, false: do not display map of maze
	// map_mode is toggled by user keyboard input, causes a call to draw_map during play mode

	//static final int viewz = 50;    
	int viewx, viewy, angle;
	int dx, dy;  // current direction
	int px, py ; // current position on maze grid (x,y)
	int walkStep;
	int viewdx, viewdy; // current view direction


	// debug stuff
	boolean deepdebug = false;
	boolean allVisible = false;
	boolean newGame = false;

	// properties of the current maze
	int mazew; // width of maze
	int mazeh; // height of maze
	Cells mazecells ; // maze as a matrix of cells which keep track of the location of walls
	Distance mazedists ; // a matrix with distance values for each cell towards the exit
	Cells seencells ; // a matrix with cells to memorize which cells are visible from the current point of view
	// the FirstPersonDrawer obtains this information and the MapDrawer uses it for highlighting currently visible walls on the map
	BSPNode rootnode ; // a binary tree type search data structure to quickly locate a subset of segments
	// a segment is a continuous sequence of walls in vertical or horizontal direction
	// a subset of segments need to be quickly identified for drawing
	// the BSP tree partitions the set of all segments and provides a binary search tree for the partitions
	

	// Mazebuilder is used to calculate a new maze together with a solution
	// The maze is computed in a separate thread. It is started in the local Build method.
	// The calculation communicates back by calling the local newMaze() method.
	MazeBuilder mazebuilder;

	
	// fixing a value matching the escape key
	final int ESCAPE = 27;

	// generation method used to compute a maze
	int method = 0 ; // 0 : default method, Falstad's original code
	// method == 1: Prim's algorithm
	int skill = 0;
	int zscale = Constants.VIEW_HEIGHT/2;

	protected RangeSet rset;
	int startx;
	int starty;
	
	/**
	 * Constructor
	 */
//	public Maze(View view, int difficulty) {
//		super() ;
//		Log.v(TAG, "Maze(View view, int difficulty)");
//		this.skill = difficulty;
//		this.view = view;
//		//panel = new MazePanel() ;
//	}
	/**
	 * Constructor that also selects a particular generation method
	 */
	public Maze(int method, View view, int difficulty)
	{
		super() ;
		this.skill = difficulty;
		this.view = view;
		// 0 is default, do not accept other settings but 0 and 1
		if (0 == method)
			this.method = 0;
		else if (1 == method)
			this.method = 1 ;
		else if (2 == method)
			this.method = 2;
//		panel = new MazePanel() ; moved to playactivity
	}
	
	public Maze(String path, View view)
	{
		super();
		Log.v(TAG, "Maze(String path, View view)");
		this.view = view;
		File f = new File(path) ;
		if (f.exists() && f.canRead())
		{		
			//panel = new MazePanel() ; moved to playactivity
			System.out.println("MazeApplication: loading maze from file: " + path);
			init();
			MazeFileReader mfr = new MazeFileReader(path) ;
			this.mazeh = mfr.getHeight() ;
			this.mazew = mfr.getWidth() ;
			Distance d = new Distance(mfr.getDistances()) ;
			this.newMaze(mfr.getRootNode(),mfr.getCells(),d,mfr.getStartX(), mfr.getStartY()) ;
		}
	}
	/**
	 * Method to initialize internal attributes. Called separately from the constructor. 
	 */
	public void init() {
		Log.v(TAG, "init()");
		//state.setState(Constants.STATE_TITLE);
		rset = new RangeSet();
		//panel.initBufferImage() ;
		addView(new MazeView(this)) ;
		notifyViewerRedraw() ;
		if (skill >= 0 && skill <= 9) {
			build(skill);
		}
	}
	
	public void setMazePanel(MazePanel panel){
		Log.v("Maze", "PlayActivity giving panel to maze object");
		this.panel = panel;
	}
	
	/**
	 * Method obtains a new Mazebuilder and has it compute new maze, 
	 * it is only used in keyDown()
	 * @param skill level determines the width, height and number of rooms for the new maze
	 */
	protected void build(int skill) {
		Log.v(TAG, "build(int skill: " + skill + ")");
		this.skill = skill;
		// switch screen
		//state.setState(Constants.STATE_GENERATING);
		//dont think above needs done, maze created in generating.
		percentdone = 0;
		notifyViewerRedraw() ;
		// select generation method
		switch(method){
		case 2: {
			Log.v(TAG, "new MazeBuilderEller(true)");
			mazebuilder = new MazeBuilderEller(true);
			break;
		}
		case 1 : {
			Log.v(TAG, "new MazeBuilderPrim()");
			mazebuilder = new MazeBuilderPrim();// generate with Prim's algorithm 
			break;
		}
		case 0: // generate with Falstad's original algorithm (0 and default), note the missing break statement
			//was not true/deterministic
		default : {
			mazebuilder = new MazeBuilder(true); 
		}
		break ;
		}
		// adjust settings and launch generation in a separate thread
		mazew = Constants.SKILL_X[skill];
		mazeh = Constants.SKILL_Y[skill];
		mazebuilder.build(this, mazew, mazeh, Constants.SKILL_ROOMS[skill], Constants.SKILL_PARTCT[skill]);
		// mazebuilder performs in a separate thread and calls back by calling newMaze() to return newly generated maze
	}
	
	/**
	 * Call back method for MazeBuilder to communicate newly generated maze as reaction to a call to build()
	 * @param root node for traversals, used for the first person perspective
	 * @param cells encodes the maze with its walls and border
	 * @param dists encodes the solution by providing distances to the exit for each position in the maze
	 * @param startx current position, x coordinate
	 * @param starty current position, y coordinate
	 */
	public void newMaze(BSPNode root, Cells c, Distance dists, int startx, int starty) {
		savedMazeDists = dists.getDists();
		Log.v("Maze", "back to newMaze()");
		if (Cells.deepdebugWall)
		{   // for debugging: dump the sequence of all deleted walls to a log file
			// This reveals how the maze was generated
			c.saveLogFile(Cells.deepedebugWallFileName);
		}
		this.startx = startx;
		this.starty = starty;
		// adjust internal state of maze model
		showMaze = showSolution = solving = false;
		mazecells = c ;
		mazedists = dists;
		seencells = new Cells(mazew+1,mazeh+1) ;
		rootnode = root ;
		setCurrentDirection(1, 0) ;
		setCurrentPosition(startx,starty) ;
		walkStep = 0;
		viewdx = dx<<16; 
		viewdy = dy<<16;
		angle = 0;
		mapMode = false;
		
		// set the current state for the state-dependent behavior
		//state.setState(Constants.STATE_PLAY) ;
		// do above:
//		Intent intent = new Intent(view.getContext(), PlayActivity.class);
//		view.getContext().startActivity(intent);
		cleanViews() ;
		// register views for the new maze
		// mazew and mazeh have been set in build() method before mazebuider was called to generate a new maze.
		// reset map_scale in mapdrawer to a value of 10
		//will need params:
		Log.v(TAG, "addingView(new FPD)");
		addView(new FirstPersonDrawer(Constants.VIEW_WIDTH,Constants.VIEW_HEIGHT,
				Constants.MAP_UNIT,Constants.STEP_SIZE, mazecells, seencells, 10, mazedists.getDists(), mazew, mazeh, root, this)) ;
		//order of registration matters, code executed in order of appearance!
		//consider:
		addView(new MapDrawer(Constants.VIEW_WIDTH,Constants.VIEW_HEIGHT,Constants.MAP_UNIT,Constants.STEP_SIZE, mazecells, seencells, 10, mazedists.getDists(), mazew, mazeh, this)) ;

		// notify viewers
		notifyViewerRedraw() ;
	}

	/////////////////////////////// Methods for the Model-View-Controller Pattern /////////////////////////////
	/**
	 * Register a view
	 */
	public void addView(Viewer view) {
		views.add(view) ;
	}
	/**
	 * Unregister a view
	 */
	public void removeView(Viewer view) {
		views.remove(view) ;
	}
	/**
	 * Remove obsolete FirstPersonDrawer and MapDrawer
	 */
	private void cleanViews() {
		// go through views and notify each one
		Iterator<Viewer> it = views.iterator() ;
		while (it.hasNext())
		{
			Viewer v = it.next() ;
			if ((v instanceof FirstPersonDrawer) ||(v instanceof MapDrawer))
			{
				//System.out.println("Removing " + v);
				it.remove() ;
			}
		}

	}
	/**
	 * Notify all registered viewers to redraw their graphics
	 */
	protected void notifyViewerRedraw() {
		Log.v(TAG, "notifyViewrRedraw- redrawing");
		// go through views and notify each one
		Iterator<Viewer> it = views.iterator() ;
		while (it.hasNext())
		{
			Viewer v = it.next() ;
			// viewers draw on the buffer graphics
			//v.redraw(Canvas canvas, state.getState(), px, py, viewdx, viewdy, walkStep, Constants.VIEW_OFFSET, rset, angle) ;
			if (null == panel) Log.e(TAG, "panel given to redraw is null");
			v.redraw(panel, Constants.STATE_PLAY, px, py, viewdx, viewdy, walkStep, Constants.VIEW_OFFSET, rset, angle) ;
					
		}
		// update the screen with the buffer graphics
		if (null != panel) panel.update();
	}
	/** 
	 * Notify all registered viewers to increment the map scale
	 */
	private void notifyViewerIncrementMapScale() {
		// go through views and notify each one
		Iterator<Viewer> it = views.iterator() ;
		while (it.hasNext())
		{
			Viewer v = it.next() ;
			v.incrementMapScale();
		}
		// update the screen with the buffer graphics
		panel.update() ;
	}
	/** 
	 * Notify all registered viewers to decrement the map scale
	 */
	private void notifyViewerDecrementMapScale() {
		// go through views and notify each one
		Iterator<Viewer> it = views.iterator() ;
		while (it.hasNext())
		{
			Viewer v = it.next() ;
			v.decrementMapScale() ;
		}
		// update the screen with the buffer graphics
		panel.update() ;
	}
	////////////////////////////// get methods ///////////////////////////////////////////////////////////////
	boolean isInMapMode() { 
		return mapMode ; 
	} 
	boolean isInShowMazeMode() { 
		return showMaze ; 
	} 
	boolean isInShowSolutionMode() { 
		return showSolution ; 
	} 
	public String getPercentDone(){
		return String.valueOf(percentdone) ;
	}
//	public Panel getPanel() {
//		return panel ;
//	}
	////////////////////////////// set methods ///////////////////////////////////////////////////////////////
	////////////////////////////// Actions that can be performed on the maze model ///////////////////////////
	protected void setCurrentPosition(int x, int y)
	{
		px = x ;
		py = y ;
	}
	protected void setCurrentDirection(int x, int y)
	{
		dx = x ;
		dy = y ;
	}
	
	
	void buildInterrupted() {
		//state.setState(Constants.STATE_TITLE);
		//replacing above:
		Intent intent = new Intent(view.getContext(), TitleActivity.class);
		view.getContext().startActivity(intent);
		notifyViewerRedraw() ;
		mazebuilder = null;
	}

	final double radify(int x) {
		return x*Math.PI/180;
	}


	/**
	 * Allows external increase to percentage in generating mode with subsequence graphics update
	 * @param pc gives the new percentage on a range [0,100]
	 * @return true if percentage was updated, false otherwise
	 */
	public boolean increasePercentage(int pc) {
		if (percentdone < pc && pc < 100) {
			percentdone = pc;
//			if (state.getState() == Constants.STATE_GENERATING)
//			{
//				notifyViewerRedraw() ;
//			}
//			else
//				dbg("Warning: Receiving update request for increasePercentage while not in generating state, skip redraw.") ;
//			return true ;
		}
		return false ;
	}

	



	/////////////////////// Methods for debugging ////////////////////////////////
	private void dbg(String str) {
		//System.out.println(str);
	}

	private void logPosition() {
		if (!deepdebug)
			return;
		dbg("x="+viewx/Constants.MAP_UNIT+" ("+
				viewx+") y="+viewy/Constants.MAP_UNIT+" ("+viewy+") ang="+
				angle+" dx="+dx+" dy="+dy+" "+viewdx+" "+viewdy);
	}
	///////////////////////////////////////////////////////////////////////////////

	/**
	 * Helper method for walk()
	 * @param dir
	 * @return true if there is no wall in this direction
	 */
	protected boolean checkMove(int dir) {
		// obtain appropriate index for direction (CW_BOT, CW_TOP ...) 
		// for given direction parameter
		int a = angle/90;
		if (dir == -1)
			a = (a+2) & 3; // TODO: check why this works
		// check if cell has walls in this direction
		// returns true if there are no walls in this direction
		return mazecells.hasMaskedBitsFalse(px, py, Constants.MASKS[a]) ;
	}



	private void rotateStep() {
		angle = (angle+1800) % 360;
		viewdx = (int) (Math.cos(radify(angle))*(1<<16));
		viewdy = (int) (Math.sin(radify(angle))*(1<<16));
		moveStep();
	}

	private void moveStep() {
		notifyViewerRedraw() ;
		try {
			Thread.currentThread().sleep(25);
		} catch (Exception e) { }
	}

	private void rotateFinish() {
		setCurrentDirection((int) Math.cos(radify(angle)), (int) Math.sin(radify(angle))) ;
		logPosition();
	}

	protected void walkFinish(int dir) {
		setCurrentPosition(px + dir*dx, py + dir*dy) ;
		
		if (isEndPosition(px,py)) {
			//state.setState(Constants.STATE_FINISH);
			//change above:
			this.view = panel;
			//below 2 lines take down map and solution before player exits maze
			if (mapMode) keyDown('m');
			if (showSolution) keyDown('t');
			Intent intent = new Intent(view.getContext(), FinishActivity.class);
			intent.putExtra("Success", "Successful");
			intent.putExtra("Distance", distTravelled);
			view.getContext().startActivity(intent);
			notifyViewerRedraw() ;
		}
		walkStep = 0;
		logPosition();
	}

	/**
	 * checks if the given position is outside the maze
	 * @param x
	 * @param y
	 * @return true if position is outside, false otherwise
	 */
	protected boolean isEndPosition(int x, int y) {
		return x < 0 || y < 0 || x >= mazew || y >= mazeh;
	}



	synchronized protected void walk(int dir) {
		distTravelled ++;
		Log.v(TAG, "walk("+dir+")");
		if (!checkMove(dir))
			return;
		for (int step = 0; step != 4; step++) {
			walkStep += dir;
			moveStep();
		}
		walkFinish(dir);
	}

	synchronized private void rotate(int dir) {
		Log.v(TAG, "rotate("+dir+")");
		final int originalAngle = angle;
		final int steps = 4;

		for (int i = 0; i != steps; i++) {
			angle = originalAngle + dir*(90*(i+1))/steps;
			rotateStep();
		}
		rotateFinish();
	}

	public boolean onKeyDown(int key) {
		Log.v(TAG, "keyDown(int key): " + key);
		boolean continueRunning = true;
		if (key == Constants.CODE_RETURNTOTITLESCREEN) {
			Intent intent = new Intent(view.getContext(), TitleActivity.class);
			view.getContext().startActivity(intent);
		}
		if (key >= '0' && key <= '9') {
			build(key - '0');
		}
		else if (key >= 'a' && key <= 'f') {
			build(key - 'a' + 10);
		}
		return continueRunning;
	}

	/**
	 * Method incorporates all reactions to keyboard input in original code, 
	 * after refactoring, Java Applet and Java Application wrapper call this method to communicate input.
	 */
	public void keyDown(int key) {
		switch (key) {
		case 'r': 
			rotate(-1);
			break;
		case 'l':
			rotate(1);
			break;
		case 'f':
			walk(1);
			break;
		case 'b':
			walk(-1);
			break;
		case 'e':
			Intent intent = new Intent(view.getContext(), TitleActivity.class);
			view.getContext().startActivity(intent);
		case 'm':
			Log.v(TAG, "mapMode: "+ mapMode);
			mapMode = !mapMode; 		
			notifyViewerRedraw() ; 
			break;
		case 's':
			showSolution = !showSolution;
			notifyViewerRedraw();
			break;
		case '+':
			notifyViewerIncrementMapScale() ;
			notifyViewerRedraw() ; // seems useless but it is necessary to make the screen update
			break ;
		case '-':
			notifyViewerDecrementMapScale() ;
			notifyViewerRedraw() ; // seems useless but it is necessary to make the screen update
			break;
		case 'n':
			showMaze = !showMaze; 		
			notifyViewerRedraw() ; 
			break;
		}
	}
//	public boolean keyDown(Event evt, int key) {
//		
//		//Returns false if the user decides to exit the program
//		boolean continueRunning = true;
//		if (key == Constants.CODE_RETURNTOTITLESCREEN)
//		{
//			state.setState(Constants.STATE_TITLE);
//			notifyViewerRedraw() ;
//		}
//		else{
//		switch (state.getState()) {
//		// if screen shows title page, keys describe level of expertise
//		// create a maze according to the user's selected level
//		case Constants.STATE_TITLE:
//			if (key >= '0' && key <= '9') {
//				build(key - '0');
//				break;
//			}
//			if (key >= 'a' && key <= 'f') {
//				build(key - 'a' + 10);
//				break;
//			}
//			break;
//		// if we are currently generating a maze, recognize interrupt signal (ESCAPE key)
//		// to stop generation of current maze
//		case Constants.STATE_GENERATING:
//			if (key == ESCAPE) {
//				mazebuilder.interrupt();
//				buildInterrupted();
//			}
//			break;
//		// if user explores maze, 
//		// react to input for directions and interrupt signal (ESCAPE key)	
//		// react to input for displaying a map of the current path or of the overall maze (on/off toggle switch)
//		// react to input to display solution (on/off toggle switch)
//		// react to input to increase/reduce map scale
//		case Constants.STATE_PLAY:
//			switch (key) {
//			case Event.UP: case 'k': case '8':
//				walk(1);
//				break;
//			case Event.LEFT: case 'h': case '4':
//				rotate(1);
//				break;
//			case Event.RIGHT: case 'l': case '6':
//				rotate(-1);
//				break;
//			case Event.DOWN: case 'j': case '2':
//				walk(-1);
//				break;
//			case ESCAPE: case 65385:
//				if (solving)
//					solving = false;
//				else
////					state.setState(Constants.STATE_TITLE);
////				notifyViewerRedraw() ;
//				break;
//			case ('w' & 0x1f): 
//			{ 
//				setCurrentPosition(px + dx, py + dy) ;
////				notifyViewerRedraw() ;
//				break;
//			}
//			case '\t': case 'm':
//				mapMode = !mapMode; 		
////				notifyViewerRedraw() ; 
//				break;
//			case 'z':
//				showMaze = !showMaze; 		
////				notifyViewerRedraw() ; 
//				break;
//			case 's':
//				showSolution = !showSolution; 		
////				notifyViewerRedraw() ;
//				break;
//			case ('s' & 0x1f):
//				if (solving)
//					solving = false;
//				else {
//					solving = true;
//				}
//			break;
//			case '+': case '=':
//			{
////				notifyViewerIncrementMapScale() ;
////				notifyViewerRedraw() ; // seems useless but it is necessary to make the screen update
//				break ;
//			}
//			case '-':
////				notifyViewerDecrementMapScale() ;
////				notifyViewerRedraw() ; // seems useless but it is necessary to make the screen update
//				break ;
//			}
//			break;
//		// if we are finished, return to initial state with title screen	
//		case Constants.STATE_FINISH:
//			//System.out.println("key: "+ key);
//			assert( (key == Constants.CODE_STOPPLAYING || key == Constants.CODE_CONTINUEPLAYING));
//			if (key == Constants.CODE_STOPPLAYING)
//			{
//				continueRunning = false;
//				System.exit(0);
//			}
//			else if(key == Constants.CODE_CONTINUEPLAYING)
//			{
////				state.setState(Constants.STATE_TITLE);
////				notifyViewerRedraw() ;
//				break;
//			}
//		} 
//		}
//		return continueRunning;
//	}

	public int getWidth()
	{
		return mazew;
	}
	
	public int getHeight()
	{
		return mazeh;
	}

	public void printWalls()
	{
		int i, j;
		int width = mazew;
		int height = mazeh;
		System.out.println("Maze Info\n" + "==========\n");
		System.out.println("Width:" + width);
		System.out.println("Height:" + height);
		for (i=0; i <= height-1;i++)
		{
			for (j=0; j <= width-1; j++ )
			{
				System.out.println("Cell: "+ j + " , "+ i + "\n");
				System.out.println("Cell has a top wall: " + mazecells.hasWallOnTop(j, i));
				System.out.println("Cell has a Bottom wall: " +mazecells.hasWallOnBottom(j, i));
				System.out.println("Cell has a Left wall: " +mazecells.hasWallOnLeft(j, i));
				System.out.println("Cell has a Right wall: " +mazecells.hasWallOnRight(j, i));
				System.out.println("End Cell: "+ j + i + "\n");
			}
		}
	}
	
	public int[] getDirection()
	{
		if (dx == 0 && dy == -1)
		{
			return Constants.TOP;
		}
		else if (dx == 0 && dy == 1)
		{
			return Constants.BOTTOM;
		}
		else if (dx == 1 && dy == 0)
		{
			return Constants.RIGHT;
		}
		else if(dx == -1 && dy == 0)
		{
			return Constants.LEFT;
		}
		else
		{
			System.out.println("Invalid Parameter");
			return (new int[] {dx,dy});
		}
	}
	
	public int getDistTravelled() {
		return distTravelled;
	}

}
